/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package qunar.tc.bistoury.instrument.client.spring.el;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.List;

/**
 * Utility methods used by the reflection resolver code to discover the appropriate
 * methods/constructors and fields that should be used in expressions.
 *
 * @author Andy Clement
 * @author Juergen Hoeller
 * @since 3.0
 */
class ReflectionHelper {

    /**
     * Compare argument arrays and return information about whether they match.
     * A supplied type converter and conversionAllowed flag allow for matches to take
     * into account that a type may be transformed into a different type by the converter.
     *
     * @param expectedArgTypes the types the method/constructor is expecting
     * @param suppliedArgTypes the types that are being supplied at the point of invocation
     * @param typeConverter    a registered type converter
     * @return a MatchInfo object indicating what kind of match it was,
     * or {@code null} if it was not a match
     */
    static ArgumentsMatchInfo compareArguments(
            List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {

        Assert.isTrue(expectedArgTypes.size() == suppliedArgTypes.size(),
                "Expected argument types and supplied argument types should be arrays of same length");

        ArgumentsMatchKind match = ArgumentsMatchKind.EXACT;
        for (int i = 0; i < expectedArgTypes.size() && match != null; i++) {
            TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
            TypeDescriptor expectedArg = expectedArgTypes.get(i);
            if (!expectedArg.equals(suppliedArg)) {
                // The user may supply null - and that will be ok unless a primitive is expected
                if (suppliedArg == null) {
                    if (expectedArg.isPrimitive()) {
                        match = null;
                    }
                } else {
                    if (suppliedArg.isAssignableTo(expectedArg)) {
                        if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
                            match = ArgumentsMatchKind.CLOSE;
                        }
                    } else if (typeConverter.canConvert(suppliedArg, expectedArg)) {
                        match = ArgumentsMatchKind.REQUIRES_CONVERSION;
                    } else {
                        match = null;
                    }
                }
            }
        }
        return (match != null ? new ArgumentsMatchInfo(match) : null);
    }

    /**
     * Based on {@link MethodInvoker#getTypeDifferenceWeight(Class[], Object[])} but operates on TypeDescriptors.
     */
    public static int getTypeDifferenceWeight(List<TypeDescriptor> paramTypes, List<TypeDescriptor> argTypes) {
        int result = 0;
        for (int i = 0; i < paramTypes.size(); i++) {
            TypeDescriptor paramType = paramTypes.get(i);
            TypeDescriptor argType = (i < argTypes.size() ? argTypes.get(i) : null);
            if (argType == null) {
                if (paramType.isPrimitive()) {
                    return Integer.MAX_VALUE;
                }
            } else {
                Class<?> paramTypeClazz = paramType.getType();
                if (!ClassUtils.isAssignable(paramTypeClazz, argType.getType())) {
                    return Integer.MAX_VALUE;
                }
                if (paramTypeClazz.isPrimitive()) {
                    paramTypeClazz = Object.class;
                }
                Class<?> superClass = argType.getType().getSuperclass();
                while (superClass != null) {
                    if (paramTypeClazz.equals(superClass)) {
                        result = result + 2;
                        superClass = null;
                    } else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) {
                        result = result + 2;
                        superClass = superClass.getSuperclass();
                    } else {
                        superClass = null;
                    }
                }
                if (paramTypeClazz.isInterface()) {
                    result = result + 1;
                }
            }
        }
        return result;
    }

    /**
     * Compare argument arrays and return information about whether they match.
     * A supplied type converter and conversionAllowed flag allow for matches to
     * take into account that a type may be transformed into a different type by the
     * converter. This variant of compareArguments also allows for a varargs match.
     *
     * @param expectedArgTypes the types the method/constructor is expecting
     * @param suppliedArgTypes the types that are being supplied at the point of invocation
     * @param typeConverter    a registered type converter
     * @return a MatchInfo object indicating what kind of match it was,
     * or {@code null} if it was not a match
     */
    static ArgumentsMatchInfo compareArgumentsVarargs(
            List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {

        Assert.isTrue(expectedArgTypes != null && expectedArgTypes.size() > 0,
                "Expected arguments must at least include one array (the vargargs parameter)");
        Assert.isTrue(expectedArgTypes.get(expectedArgTypes.size() - 1).isArray(),
                "Final expected argument should be array type (the varargs parameter)");

        ArgumentsMatchKind match = ArgumentsMatchKind.EXACT;

        // Check up until the varargs argument:

        // Deal with the arguments up to 'expected number' - 1 (that is everything but the varargs argument)
        int argCountUpToVarargs = expectedArgTypes.size() - 1;
        for (int i = 0; i < argCountUpToVarargs && match != null; i++) {
            TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
            TypeDescriptor expectedArg = expectedArgTypes.get(i);
            if (suppliedArg == null) {
                if (expectedArg.isPrimitive()) {
                    match = null;
                }
            } else {
                if (!expectedArg.equals(suppliedArg)) {
                    if (suppliedArg.isAssignableTo(expectedArg)) {
                        if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
                            match = ArgumentsMatchKind.CLOSE;
                        }
                    } else if (typeConverter.canConvert(suppliedArg, expectedArg)) {
                        match = ArgumentsMatchKind.REQUIRES_CONVERSION;
                    } else {
                        match = null;
                    }
                }
            }
        }

        // If already confirmed it cannot be a match, then return
        if (match == null) {
            return null;
        }

        if (suppliedArgTypes.size() == expectedArgTypes.size() &&
                expectedArgTypes.get(expectedArgTypes.size() - 1).equals(
                        suppliedArgTypes.get(suppliedArgTypes.size() - 1))) {
            // Special case: there is one parameter left and it is an array and it matches the varargs
            // expected argument - that is a match, the caller has already built the array. Proceed with it.
        } else {
            // Now... we have the final argument in the method we are checking as a match and we have 0
            // or more other arguments left to pass to it.
            TypeDescriptor varargsDesc = expectedArgTypes.get(expectedArgTypes.size() - 1);
            Class<?> varargsParamType = varargsDesc.getElementTypeDescriptor().getType();

            // All remaining parameters must be of this type or convertible to this type
            for (int i = expectedArgTypes.size() - 1; i < suppliedArgTypes.size(); i++) {
                TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
                if (suppliedArg == null) {
                    if (varargsParamType.isPrimitive()) {
                        match = null;
                    }
                } else {
                    if (varargsParamType != suppliedArg.getType()) {
                        if (ClassUtils.isAssignable(varargsParamType, suppliedArg.getType())) {
                            if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
                                match = ArgumentsMatchKind.CLOSE;
                            }
                        } else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsParamType))) {
                            match = ArgumentsMatchKind.REQUIRES_CONVERSION;
                        } else {
                            match = null;
                        }
                    }
                }
            }
        }

        return (match != null ? new ArgumentsMatchInfo(match) : null);
    }


    // TODO could do with more refactoring around argument handling and varargs

    /**
     * Convert a supplied set of arguments into the requested types. If the parameterTypes are related to
     * a varargs method then the final entry in the parameterTypes array is going to be an array itself whose
     * component type should be used as the conversion target for extraneous arguments. (For example, if the
     * parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both
     * the boolean and float must be converted to strings). This method does *not* repackage the arguments
     * into a form suitable for the varargs invocation - a subsequent call to setupArgumentsForVarargsInvocation handles that.
     *
     * @param converter the converter to use for type conversions
     * @param arguments the arguments to convert to the requested parameter types
     * @param method    the target Method
     * @return true if some kind of conversion occurred on the argument
     * @throws SpelEvaluationException if there is a problem with conversion
     */
    public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method)
            throws SpelEvaluationException {

        Integer varargsPosition = (method.isVarArgs() ? method.getParameterTypes().length - 1 : null);
        return convertArguments(converter, arguments, method, varargsPosition);
    }

    /**
     * Takes an input set of argument values and converts them to the types specified as the
     * required parameter types. The arguments are converted 'in-place' in the input array.
     *
     * @param converter       the type converter to use for attempting conversions
     * @param arguments       the actual arguments that need conversion
     * @param methodOrCtor    the target Method or Constructor
     * @param varargsPosition the known position of the varargs argument, if any
     *                        ({@code null} if not varargs)
     * @return {@code true} if some kind of conversion occurred on an argument
     * @throws EvaluationException if a problem occurs during conversion
     */
    static boolean convertArguments(TypeConverter converter, Object[] arguments, Object methodOrCtor,
                                    Integer varargsPosition) throws EvaluationException {

        boolean conversionOccurred = false;
        if (varargsPosition == null) {
            for (int i = 0; i < arguments.length; i++) {
                TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i));
                Object argument = arguments[i];
                arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
                conversionOccurred |= (argument != arguments[i]);
            }
        } else {
            // Convert everything up to the varargs position
            for (int i = 0; i < varargsPosition; i++) {
                TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i));
                Object argument = arguments[i];
                arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
                conversionOccurred |= (argument != arguments[i]);
            }
            MethodParameter methodParam = MethodParameter.forMethodOrConstructor(methodOrCtor, varargsPosition);
            if (varargsPosition == arguments.length - 1) {
                // If the target is varargs and there is just one more argument
                // then convert it here
                TypeDescriptor targetType = new TypeDescriptor(methodParam);
                Object argument = arguments[varargsPosition];
                TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
                arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
                // Three outcomes of that previous line:
                // 1) the input argument was already compatible (ie. array of valid type) and nothing was done
                // 2) the input argument was correct type but not in an array so it was made into an array
                // 3) the input argument was the wrong type and got converted and put into an array
                if (argument != arguments[varargsPosition] &&
                        !isFirstEntryInArray(argument, arguments[varargsPosition])) {
                    conversionOccurred = true; // case 3
                }
            } else {
                // Convert remaining arguments to the varargs element type
                TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor();
                for (int i = varargsPosition; i < arguments.length; i++) {
                    Object argument = arguments[i];
                    arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
                    conversionOccurred |= (argument != arguments[i]);
                }
            }
        }
        return conversionOccurred;
    }

    /**
     * Check if the supplied value is the first entry in the array represented by the possibleArray value.
     *
     * @param value         the value to check for in the array
     * @param possibleArray an array object that may have the supplied value as the first element
     * @return true if the supplied value is the first entry in the array
     */
    private static boolean isFirstEntryInArray(Object value, Object possibleArray) {
        if (possibleArray == null) {
            return false;
        }
        Class<?> type = possibleArray.getClass();
        if (!type.isArray() || Array.getLength(possibleArray) == 0 ||
                !ClassUtils.isAssignableValue(type.getComponentType(), value)) {
            return false;
        }
        Object arrayValue = Array.get(possibleArray, 0);
        return (type.getComponentType().isPrimitive() ? arrayValue.equals(value) : arrayValue == value);
    }

    /**
     * Package up the arguments so that they correctly match what is expected in parameterTypes.
     * For example, if parameterTypes is {@code (int, String[])} because the second parameter
     * was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be
     * repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types.
     *
     * @param requiredParameterTypes the types of the parameters for the invocation
     * @param args                   the arguments to be setup ready for the invocation
     * @return a repackaged array of arguments where any varargs setup has been done
     */
    public static Object[] setupArgumentsForVarargsInvocation(Class<?>[] requiredParameterTypes, Object... args) {
        // Check if array already built for final argument
        int parameterCount = requiredParameterTypes.length;
        int argumentCount = args.length;

        // Check if repackaging is needed...
        if (parameterCount != args.length ||
                requiredParameterTypes[parameterCount - 1] !=
                        (args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) {

            int arraySize = 0;  // zero size array if nothing to pass as the varargs parameter
            if (argumentCount >= parameterCount) {
                arraySize = argumentCount - (parameterCount - 1);
            }

            // Create an array for the varargs arguments
            Object[] newArgs = new Object[parameterCount];
            System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1);

            // Now sort out the final argument, which is the varargs one. Before entering this method,
            // the arguments should have been converted to the box form of the required type.
            Class<?> componentType = requiredParameterTypes[parameterCount - 1].getComponentType();
            Object repackagedArgs = Array.newInstance(componentType, arraySize);
            for (int i = 0; i < arraySize; i++) {
                Array.set(repackagedArgs, i, args[parameterCount - 1 + i]);
            }
            newArgs[newArgs.length - 1] = repackagedArgs;
            return newArgs;
        }
        return args;
    }


    static enum ArgumentsMatchKind {

        /**
         * An exact match is where the parameter types exactly match what the method/constructor is expecting
         */
        EXACT,

        /**
         * A close match is where the parameter types either exactly match or are assignment-compatible
         */
        CLOSE,

        /**
         * A conversion match is where the type converter must be used to transform some of the parameter types
         */
        REQUIRES_CONVERSION
    }


    /**
     * An instance of ArgumentsMatchInfo describes what kind of match was achieved
     * between two sets of arguments - the set that a method/constructor is expecting
     * and the set that are being supplied at the point of invocation. If the kind
     * indicates that conversion is required for some of the arguments then the arguments
     * that require conversion are listed in the argsRequiringConversion array.
     */
    static class ArgumentsMatchInfo {

        private final ArgumentsMatchKind kind;

        ArgumentsMatchInfo(ArgumentsMatchKind kind) {
            this.kind = kind;
        }

        public boolean isExactMatch() {
            return (this.kind == ArgumentsMatchKind.EXACT);
        }

        public boolean isCloseMatch() {
            return (this.kind == ArgumentsMatchKind.CLOSE);
        }

        public boolean isMatchRequiringConversion() {
            return (this.kind == ArgumentsMatchKind.REQUIRES_CONVERSION);
        }

        @Override
        public String toString() {
            return "ArgumentMatchInfo: " + this.kind;
        }
    }

}
